﻿#region Copyright information
// 
// Copyright © 2005-2013 Yongkee Cho. All rights reserved.
// 
// This code is a part of the Biological Object Library and governed under the terms of the
// GNU Lesser General  Public License (LGPL) version 2.1 which accompanies this distribution.
// For more information on the LGPL, please visit http://bol.codeplex.com/license.
// 
// - Filename: NearestCentroidClassifier.cs
// - Author: Yongkee Cho
// - Email: yongkeecho@gmail.com
// - Date Created: 2012-12-08 8:26 AM
// - Last Modified: 2013-01-25 3:59 PM
// 
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using BOL.Linq;
using BOL.Linq.Descriptive;
using BOL.Linq.Distances;

namespace BOL.Algorithms.Classification
{
    public class NearestCentroidClassifier<TFeature, TClass> : ClassifierBase<TFeature, TClass>, ICloneable, IEquatable<NearestCentroidClassifier<TFeature, TClass>>
    {
        #region Public properties

        public IDictionary<TClass, double[]> Centroids { get; private set; }

        public Func<TFeature, double> Convert { get; private set; }

        #endregion

        #region Constructor

        public NearestCentroidClassifier(IDictionary<TClass, double[]> centroids, Func<TFeature, double> convert)
        {
            Centroids = centroids;
            Convert = convert;
        }

        #endregion

        #region ICloneable implementation

        public NearestCentroidClassifier<TFeature, TClass> Clone()
        {
            return new NearestCentroidClassifier<TFeature, TClass>(Centroids.ToDictionary(x => x.Key, x => x.Value), Convert);
        }

        object ICloneable.Clone()
        {
            return Clone();
        }

        #endregion

        #region Public methods

        public static NearestCentroidClassifier<TFeature, TClass> Train<TSource>(IEnumerable<TSource> source, Func<TSource, TClass> classSelector, Func<TSource, TFeature[]> featuresSelector, Func<TFeature, double> convert)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            if (classSelector == null)
                throw new ArgumentNullException("classSelector");
            if (featuresSelector == null)
                throw new ArgumentNullException("featuresSelector");
            if (convert == null)
                throw new ArgumentNullException("convert");

            var list = source.GroupBy(classSelector).Select(x => new { Class = x.Key, Features = x.Select(featuresSelector).Select(y => y.Select(convert).ToArray()).ToList() }).ToList();
            var length = list.First().Features.First().Length;
            var centroids = list.ToDictionary(x => x.Class, x => Enumerable.Range(0, length).Select(i => x.Features.Select(y => y[i]).Average()).ToArray());

            return new NearestCentroidClassifier<TFeature, TClass>(centroids, convert);
        }

        public override TClass Classify(TFeature[] features)
        {
            return Centroids.ToDictionary(x => x.Key, x => x.Value.Euclidean(features.Select(Convert))).ArgMin();
        }

        #endregion

        #region IEquatable<NearestCentroidClassifier<TFeature, TClass>> implementation

        public bool Equals(NearestCentroidClassifier<TFeature, TClass> other)
        {
            if (other == null)
                throw new ArgumentNullException("other");

            return Centroids.DictionaryEqual(other.Centroids);
        }

        #endregion

        #region Object overriden

        public override int GetHashCode()
        {
            return Centroids.OrderBy(x => x.Key).Aggregate(1, (hashCode, x) => hashCode ^ x.Key.GetHashCode());
        }

        public override bool Equals(object other)
        {
            if (other == null)
                throw new ArgumentNullException("other");

            if (!(other is NearestCentroidClassifier<TFeature, TClass>))
                throw new InvalidCastException("The 'other' argument is not a NearestCentroidClassifier<TFeature, TClass> object.");

            return Equals(other as NearestCentroidClassifier<TFeature, TClass>);
        }

        public override string ToString()
        {
            return String.Join("\n", Centroids.Select(x => String.Format("Centroid_{0}=({1})", x.Key, String.Join(", ", x.Value))));
        }

        #endregion
    }
}